Dansk

Mestr React Suspense til datahentning. Lær at håndtere loading states deklarativt, forbedre UX med transitions og håndtere fejl med Error Boundaries.

React Suspense Boundaries: En Dybdegående Guide til Deklarativ Håndtering af Indlæsningstilstande

I en verden af moderne webudvikling er det altafgørende at skabe en problemfri og responsiv brugeroplevelse. En af de mest vedholdende udfordringer, udviklere står over for, er håndtering af indlæsningstilstande. Fra hentning af data til en brugerprofil til indlæsning af en ny sektion i en applikation, er ventetiderne kritiske. Historisk set har dette involveret et sammenfiltret net af booleske flag som isLoading, isFetching og hasError, spredt ud over vores komponenter. Denne imperative tilgang roder i vores kode, komplicerer logikken og er en hyppig kilde til fejl, såsom race conditions.

Her kommer React Suspense ind i billedet. Oprindeligt introduceret til code-splitting med React.lazy(), er dets kapabiliteter blevet udvidet dramatisk med React 18 til at blive en kraftfuld, førsteklasses mekanisme til håndtering af asynkrone operationer, især datahentning. Suspense giver os mulighed for at håndtere indlæsningstilstande på en deklarativ måde, hvilket fundamentalt ændrer, hvordan vi skriver og ræsonnerer om vores komponenter. I stedet for at spørge "Er jeg i gang med at indlæse?", kan vores komponenter simpelthen sige: "Jeg har brug for disse data for at rendere. Mens jeg venter, bedes du vise dette fallback-UI."

Denne omfattende guide vil tage dig med på en rejse fra de traditionelle metoder til state management til det deklarative paradigme i React Suspense. Vi vil udforske, hvad Suspense boundaries er, hvordan de fungerer for både code-splitting og datahentning, og hvordan man orkestrerer komplekse indlæsnings-UI'er, der glæder dine brugere i stedet for at frustrere dem.

Den Gamle Måde: Besværet med Manuelle Indlæsningstilstande

Før vi fuldt ud kan værdsætte elegancen ved Suspense, er det vigtigt at forstå det problem, det løser. Lad os se på en typisk komponent, der henter data ved hjælp af useEffect og useState hooks.

Forestil dig en komponent, der skal hente og vise brugerdata:


import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Nulstil state for nyt userId
    setIsLoading(true);
    setUser(null);
    setError(null);

    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Netværksresponsen var ikke ok');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Gen-hent, når userId ændres

  if (isLoading) {
    return <p>Indlæser profil...</p>;
  }

  if (error) {
    return <p>Fejl: {error.message}</p>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

Dette mønster er funktionelt, men det har flere ulemper:

Introduktion til React Suspense: Et Paradigmeskift

Suspense vender denne model på hovedet. I stedet for at komponenten håndterer indlæsningstilstanden internt, kommunikerer den sin afhængighed af en asynkron operation direkte til React. Hvis de data, den har brug for, endnu ikke er tilgængelige, "suspenderer" komponenten renderingen.

Når en komponent suspenderer, går React op i komponenttræet for at finde den nærmeste Suspense Boundary. En Suspense Boundary er en komponent, du definerer i dit træ ved hjælp af <Suspense>. Denne boundary vil derefter rendere et fallback-UI (som en spinner eller en skeleton loader), indtil alle komponenter inden i den har løst deres dataafhængigheder.

Kerneideen er at samlokalisere dataafhængigheden med den komponent, der har brug for den, samtidig med at indlæsnings-UI'et centraliseres på et højere niveau i komponenttræet. Dette rydder op i komponentlogikken og giver dig kraftfuld kontrol over brugerens indlæsningsoplevelse.

Hvordan "Suspenderer" en Komponent?

Magien bag Suspense ligger i et mønster, der kan virke usædvanligt i starten: at kaste et Promise. En Suspense-aktiveret datakilde fungerer således:

  1. Når en komponent beder om data, tjekker datakilden, om den har dataene cachet.
  2. Hvis dataene er tilgængelige, returnerer den dem synkront.
  3. Hvis dataene ikke er tilgængelige (dvs. de er i øjeblikket ved at blive hentet), kaster datakilden det Promise, der repræsenterer den igangværende fetch-anmodning.

React fanger dette kastede Promise. Det crasher ikke din app. I stedet fortolker det det som et signal: "Denne komponent er ikke klar til at rendere endnu. Sæt den på pause, og led efter en Suspense boundary ovenfor den for at vise et fallback." Når Promiset resolver, vil React forsøge at rendere komponenten igen, som nu vil modtage sine data og rendere succesfuldt.

`<Suspense>` Boundary: Din Deklarator for Indlæsnings-UI

<Suspense>-komponenten er hjertet i dette mønster. Den er utrolig simpel at bruge og tager en enkelt, påkrævet prop: fallback.


import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>Min Applikation</h1>
      <Suspense fallback={<p>Indlæser indhold...</p>}>
        <SomeComponentThatFetchesData />
      </Suspense>
    </div>
  );
}

I dette eksempel, hvis SomeComponentThatFetchesData suspenderer, vil brugeren se beskeden "Indlæser indhold...", indtil dataene er klar. Fallback'et kan være enhver gyldig React-node, fra en simpel streng til en kompleks skeleton-komponent.

Klassisk Anvendelse: Code Splitting med React.lazy()

Den mest etablerede brug af Suspense er til code splitting. Det giver dig mulighed for at udsætte indlæsningen af JavaScript for en komponent, indtil den rent faktisk er nødvendig.


import React, { Suspense, lazy } from 'react';

// Denne komponents kode vil ikke være i det oprindelige bundle.
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h2>Noget indhold, der indlæses med det samme</h2>
      <Suspense fallback={<div>Indlæser komponent...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Her vil React kun hente JavaScript'et for HeavyComponent, når den første gang forsøger at rendere den. Mens den hentes og parses, vises Suspense fallback'et. Dette er en kraftfuld teknik til at forbedre den indledende sideindlæsningstid.

Den Moderne Frontlinje: Datahentning med Suspense

Selvom React leverer Suspense-mekanismen, leverer den ikke en specifik data-fetching-klient. For at bruge Suspense til datahentning har du brug for en datakilde, der integrerer med den (dvs. en, der kaster et Promise, når data er afventende).

Frameworks som Relay og Next.js har indbygget, førsteklasses support for Suspense. Populære data-fetching-biblioteker som TanStack Query (tidligere React Query) og SWR tilbyder også eksperimentel eller fuld support for det.

For at forstå konceptet, lad os skabe en meget simpel, konceptuel wrapper omkring fetch API'en for at gøre den Suspense-kompatibel. Bemærk: Dette er et forenklet eksempel til uddannelsesmæssige formål og er ikke produktionsklar. Det mangler korrekt caching og finesser inden for fejlhåndtering.


// data-fetcher.js
// En simpel cache til at gemme resultater
const cache = new Map();

export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
  }

  const record = cache.get(url);

  if (record.status === 'pending') {
    throw record.promise; // Dette er magien!
  }
  if (record.status === 'error') {
    throw record.error;
  }
  if (record.status === 'success') {
    return record.data;
  }
}

async function fetchAndCache(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Fetch fejlede med status ${response.status}`);
    }
    const data = await response.json();
    cache.set(url, { status: 'success', data });
  } catch (e) {
    cache.set(url, { status: 'error', error: e });
  }
}

Denne wrapper vedligeholder en simpel status for hver URL. Når fetchData kaldes, tjekker den status. Hvis den er afventende, kaster den promiset. Hvis den er succesfuld, returnerer den dataene. Lad os nu omskrive vores UserProfile-komponent ved hjælp af dette.


// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';

// Komponenten, der rent faktisk bruger dataene
function ProfileDetails({ userId }) {
  // Prøv at læse data. Hvis de ikke er klar, vil dette suspendere.
  const user = fetchData(`https://api.example.com/users/${userId}`);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

// Forælderkomponenten, der definerer UI'et for indlæsningstilstanden
export function UserProfile({ userId }) {
  return (
    <Suspense fallback={<p>Indlæser profil...</p>}>
      <ProfileDetails userId={userId} />
    </Suspense>
  );
}

Se forskellen! ProfileDetails-komponenten er ren og fokuserer udelukkende på at rendere dataene. Den har ingen isLoading- eller error-tilstande. Den anmoder simpelthen om de data, den har brug for. Ansvaret for at vise en indlæsningsindikator er blevet flyttet op til forælderkomponenten, UserProfile, som deklarativt angiver, hvad der skal vises, mens man venter.

Orkestrering af Komplekse Indlæsningstilstande

Den sande styrke ved Suspense bliver tydelig, når du bygger komplekse UI'er med flere asynkrone afhængigheder.

Nestede Suspense Boundaries for et Forskudt UI

Du kan neste Suspense boundaries for at skabe en mere raffineret indlæsningsoplevelse. Forestil dig en dashboard-side med en sidebar, et hovedindholdsområde og en liste over seneste aktiviteter. Hver af disse kan kræve sin egen datahentning.


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <div className="layout">
        <Suspense fallback={<p>Indlæser navigation...</p>}>
          <Sidebar />
        </Suspense>

        <main>
          <Suspense fallback={<ProfileSkeleton />}>
            <MainContent />
          </Suspense>

          <Suspense fallback={<ActivityFeedSkeleton />}>
            <ActivityFeed />
          </Suspense>
        </main>
      </div>
    </div>
  );
}

Med denne struktur:

Dette giver dig mulighed for at vise nyttigt indhold til brugeren så hurtigt som muligt, hvilket dramatisk forbedrer den opfattede ydeevne.

Undgå "Popcorning" af UI'et

Nogle gange kan den forskudte tilgang føre til en hakkende effekt, hvor flere spinnere vises og forsvinder i hurtig rækkefølge, en effekt der ofte kaldes "popcorning". For at løse dette kan du flytte Suspense boundary'en højere op i træet.


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <div className="layout">
          <Sidebar />
          <main>
            <MainContent />
            <ActivityFeed />
          </main>
        </div>
      </Suspense>
    </div>
  );
}

I denne version vises en enkelt DashboardSkeleton, indtil alle de underordnede komponenter (Sidebar, MainContent, ActivityFeed) har deres data klar. Hele dashboardet vises derefter på én gang. Valget mellem nestede boundaries og en enkelt boundary på et højere niveau er en UX-designbeslutning, som Suspense gør triviel at implementere.

Fejlhåndtering med Error Boundaries

Suspense håndterer den afventende (pending) tilstand af et promise, men hvad med den afviste (rejected) tilstand? Hvis det promise, der kastes af en komponent, afvises (f.eks. en netværksfejl), vil det blive behandlet som enhver anden renderingsfejl i React.

Løsningen er at bruge Error Boundaries. En Error Boundary er en klassekomponent, der definerer en speciel livscyklusmetode, componentDidCatch() eller en statisk metode getDerivedStateFromError(). Den fanger JavaScript-fejl hvor som helst i sit underordnede komponenttræ, logger disse fejl og viser et fallback-UI.

Her er en simpel Error Boundary-komponent:


import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Opdater state, så den næste rendering vil vise fallback-UI'et.
    return { hasError: true, error: error };
  }

  componentDidCatch(error, errorInfo) {
    // Du kan også logge fejlen til en fejlrapporteringstjeneste
    console.error("Caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Du kan rendere et hvilket som helst brugerdefineret fallback-UI
      return <h1>Noget gik galt. Prøv venligst igen.</h1>;
    }

    return this.props.children; 
  }
}

Du kan derefter kombinere Error Boundaries med Suspense for at skabe et robust system, der håndterer alle tre tilstande: afventende, succes og fejl.


import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';

function App() {
  return (
    <div>
      <h2>Brugerinformation</h2>
      <ErrorBoundary>
        <Suspense fallback={<p>Indlæser...</p>}>
          <UserProfile userId={123} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Med dette mønster vises profilen, hvis datahentningen inde i UserProfile lykkes. Hvis den er afventende, vises Suspense-fallback'et. Hvis den fejler, vises Error Boundary'ens fallback. Logikken er deklarativ, kompositionel og let at ræsonnere om.

Transitions: Nøglen til Ikke-Blokerende UI-Opdateringer

Der er en sidste brik i puslespillet. Overvej en brugerinteraktion, der udløser en ny datahentning, som at klikke på en "Næste"-knap for at se en anden brugerprofil. Med opsætningen ovenfor vil UserProfile-komponenten suspendere igen i det øjeblik, knappen klikkes, og userId-prop'en ændres. Dette betyder, at den aktuelt synlige profil forsvinder og erstattes af indlæsnings-fallback'et. Dette kan føles brat og forstyrrende.

Det er her, transitions kommer ind. Transitions er en ny funktion i React 18, der lader dig markere visse state-opdateringer som ikke-presserende. Når en state-opdatering er pakket ind i en transition, vil React fortsætte med at vise det gamle UI (det forældede indhold), mens det forbereder det nye indhold i baggrunden. Den vil først committe UI-opdateringen, når det nye indhold er klar til at blive vist.

Den primære API til dette er useTransition-hooket.


import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';

function ProfileSwitcher() {
  const [userId, setUserId] = useState(1);
  const [isPending, startTransition] = useTransition();

  const handleNextClick = () => {
    startTransition(() => {
      setUserId(id => id + 1);
    });
  };

  return (
    <div>
      <button onClick={handleNextClick} disabled={isPending}>
        Næste Bruger
      </button>

      {isPending && <span> Indlæser ny profil...</span>}

      <ErrorBoundary>
        <Suspense fallback={<p>Indlæser startprofil...</p>}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Her er hvad der sker nu:

  1. Den indledende profil for userId: 1 indlæses og viser Suspense-fallback'et.
  2. Brugeren klikker på "Næste Bruger".
  3. setUserId-kaldet er pakket ind i startTransition.
  4. React begynder at rendere UserProfile med det nye userId på 2 i hukommelsen. Dette får den til at suspendere.
  5. Afgørende, i stedet for at vise Suspense-fallback'et, beholder React det gamle UI (profilen for bruger 1) på skærmen.
  6. Den isPending-booleske værdi, der returneres af useTransition, bliver true, hvilket giver os mulighed for at vise en subtil, inline indlæsningsindikator uden at afmontere det gamle indhold.
  7. Når dataene for bruger 2 er hentet, og UserProfile kan rendere succesfuldt, committer React opdateringen, og den nye profil vises problemfrit.

Transitions giver det sidste lag af kontrol, hvilket gør det muligt for dig at bygge sofistikerede og brugervenlige indlæsningsoplevelser, der aldrig føles hakkende.

Bedste Praksis og Globale Overvejelser

Konklusion

React Suspense repræsenterer mere end bare en ny funktion; det er en fundamental udvikling i, hvordan vi tilgår asynkronicitet i React-applikationer. Ved at bevæge os væk fra manuelle, imperative indlæsningsflag og omfavne en deklarativ model, kan vi skrive komponenter, der er renere, mere modstandsdygtige og lettere at sammensætte.

Ved at kombinere <Suspense> for afventende tilstande, Error Boundaries for fejltilstande og useTransition for problemfri opdateringer, har du et komplet og kraftfuldt værktøjssæt til din rådighed. Du kan orkestrere alt fra simple indlæsningsspinnere til komplekse, forskudte dashboard-afsløringer med minimal, forudsigelig kode. Når du begynder at integrere Suspense i dine projekter, vil du opdage, at det ikke kun forbedrer din applikations ydeevne og brugeroplevelse, men også dramatisk forenkler din state management-logik, så du kan fokusere på det, der virkelig betyder noget: at bygge fantastiske funktioner.